Previous Book Contents Book Index Next

Inside Macintosh: AppleScript Scripting Additions Guide /
Chapter 2 - Scripting Addition Commands


Using Read/Write Commands

The commands provided by the Read/Write Commands scripting addition allow you to open a file for access, get and set its length, read data from the file, insert new data in the file, and close access to the file. These commands allow you to make use, from within a script, of some capabilities of the File Manager, the part of the Macintosh Operating System that controls files.

WARNING
The Read/Write Commands scripting addition is intended for use by experienced programmers. If you are not familiar with the File Manager as described in Inside Macintosh: Files, proceed with caution. Using these commands incorrectly may cause loss of data.<8batcolor>s
Most of the Read/Write Commands allow you to specify a file reference number instead of a reference to a file. A file reference number is an integer, assigned by the File Manager, that uniquely identifies a file. You can obtain a file reference number with the Open for Access command, then use the number returned to refer to the same file until you use the Close Access command to close the file. It is usually preferable to specify a file reference number rather than a reference to a file because it takes the Read/Write Commands scripting addition less time to locate the file.

You can use the Read/Write commands with either text-based data or binary data. Most databases can export data as text, with fields and records separated by delimiters, and some store their data as text files. The examples in this section demonstrate how to use the Read/Write commands with text-based data. These examples assume that you have basic information about the way the data is stored in a text file, such as the delimiters used to separate fields and records. You can use similar techniques to read and write binary data if you know how the data is organized within a file. For example, if you know the header format for a file of type 'PICT', you can write scripts that read and write to 'PICT' files.

Both the Read command and the Write command make use of the file mark, a marker used by the File Manager that indicates the byte at which the Read and Write commands begin operating. By default, the file mark is the first byte of the file. After the Read command reads a range of bytes or the Write command writes over a range of bytes, the file mark is set to byte just after the end of that range. The next Read or Write command begins operating at the new file mark.

For example, suppose you want to extract a particular record from a text-based database of names and addresses. To do so, you need to know the number of fields in each record, the position of the desired record in the database, and the delimiters used to separate the records in the database. You can then use the Open for Access command to get a file reference number for the file that contains the desired record and a Read command within a repeat loop to read each successive record. After reading each record, the Read command sets the file mark to the beginning of the next record. When the repeat loop determines that the desired record has been reached, it returns the data for that record. Listing 2-1 shows one way to do this.

Listing 2-1 Reading a specific record from a text-based database file

--first choose data file to work with
set pathToUse to choose file
try
   set x to open for access pathToUse
   set z to ReadRecord(10, 1, tab, return, x)
   close access x
   z --display requested record
on error errString number errNum
   display dialog errString
   close access x
end try

on ReadRecord(numberOfFields, whichRecord, fieldDelimiter, ¬ 
               recordDelimiter, fileRefNum)
   try
      (* if there's a record delimiter, read all fields except for 
      last using field delimiter, then read last field using record 
      delimiter *)
      if recordDelimiter is "" then
         set readxTimes to numberOfFields
      else
         set readxTimes to numberOfFields - 1
      end if
      repeat whichRecord times
         set recordData to {}
         repeat (readxTimes) times
            set recordData to recordData & ¬
               {(read fileRefNum before fieldDelimiter)}
         end repeat
         if readxTimes is not numberOfFields then
            set recordData to recordData & ¬
               {(read fileRefNum before recordDelimiter)}
         end if
      end repeat
      return recordData
   on error errString number errNum
      display dialog errString
      return errString
   end try
end ReadRecord
The script in Listing 2-1 begins by using the Choose File command to allow
the user to choose the text file that contains the desired record. After initializing the variable into which the record will be read, the script uses the Open
for Access command to open the file and the ReadRecord handler to read a specific record.

The ReadRecord handler shown in Listing 2-1 takes five parameters:

numberOfFields
The number of fields in the record.
whichRecord
An integer that identifies the position of the desired record.
fieldDelimiter
The delimiter used in the file to separate fields.
recordDelimiter
The delimiter (if any) used to separate records. If the file doesn't use a different delimiter to separate records, this parameter must be set to "".
fileRefNum
A file reference number obtained with the Open for Access command.
If recordDelimiter is set to "", the ReadRecord handler reads the specified number of fields for each record. If recordDelimiter is set to a delimiter, ReadRecord reads all the fields in a record but the last, then reads the last field up to the record delimiter. This is necessary to ensure that the last field of one record is not combined with the first field of the next.

The ReadRecord handler reads each new record into the variable recordData. If the record is the one requested, ReadRecord returns
that record. If the record is not the requested record, ReadRecord sets recordData to an empty list and reads the next record.

You can use similar techniques to locate the exact position of a record you want to delete from a text file. In addition to locating the record to be deleted, you need to store all the records after that record in a variable and write the contents of the variable starting at the beginning of the record to be deleted. You can then use the Get EOF and Set EOF commands to get the initial size of the file and reset its size after deleting the record. Listing 2-2 demonstrates how to do this.

Listing 2-2 Deleting a record from a text-based database file

--choose data file to use 
set pathToUse to choose file

try
   set x to open for access pathToUse with write permission
   DeleteRecord(10, 1, tab, return, x)
   close access x
on error errString number errNum
   display dialog errString
   close access x
end try

on DeleteRecord(numberOfFields, whichRecord, ¬
               fieldDelimiter, recordDelimiter, fileRefNum)
   try
      --initialize variables
      set startSize to get eof fileRefNum --current size
      set idx to 1 --counter
      set preRecordSize to 1 --offset of record to delete
      set accumulatedSize to 0 --total size of records read 
      
      if recordDelimiter is "" then
         set readxTimes to numberOfFields
      else
         set readxTimes to numberOfFields - 1
      end if
      

      repeat with idx from 1 to whichRecord
         repeat (readxTimes) times
            set q to read fileRefNum until fieldDelimiter
            set accumulatedSize to accumulatedSize + (length of q)
         end repeat

         if readxTimes is not numberOfFields then
            set q to read fileRefNum until recordDelimiter
            set accumulatedSize to accumulatedSize + (length of q)
         end if

         (* if record to delete is the first record in file or the 
         next record that will be read, set preRecordSize *)
         if whichRecord is 1 or idx is whichRecord - 1 then
            if whichRecord is 1 then 
               set preRecordSize to 1
            else
               set preRecordSize to accumulatedSize
            end if
         end if
      end repeat
      
      (* now that preRecordSize is determined, read the record to be 
      deleted so file mark is set to beginning of next record *)
      set fileBuffer to read fileRefNum from accumulatedSize + 1

      --next, overwrite record to be deleted with remainder of file 
      if (startSize - accumulatedSize) is not 0 then
         write fileBuffer to fileRefNum starting at preRecordSize
         set eof fileRefNum to (startSize - accumulatedSize)
      else
         (* if the file contains only the record to be 
         deleted, set the end of the file to 0 *)
         if whichRecord is 1 then 
            set eof fileRefNum to 0
         (* if record to be deleted is last record in file, 
         just shrink the file *)
         else
            set eof fileRefNum to preRecordSize
         end if
      end if
   on error errString number errNum
      display dialog errString
   end try
end DeleteRecord
The DeleteRecord handler shown in Listing 2-2 takes five parameters:

numberOfFields
The number of fields in each record.
whichRecord
An integer that identifies the position of the record you want
to delete.
fieldDelimiter
The delimiter used in the file to separate fields.
recordDelimiter
The delimiter (if any) used to separate records. If the file doesn't use a different delimiter to separate records, this parameter must be set to "".
fileRefNum
A file reference number obtained with the Open for Access command.
Like the ReadRecord handler in Listing 2-1, the DeleteRecord handler reads the specified number of fields for each record if recordDelimiter is set to "". If recordDelimiter is set to a delimiter, DeleteRecord reads all the fields in a record but the last, then reads the last field up to the record delimiter. The size of each successive record is added to the accumulatedSize variable, which contains the total size of the previously read records.

When it reaches the record to be deleted, DeleteRecord stores the contents of accumulatedSize in the preRecordSize variable, reads through the record to set the file mark, reads from the file mark to the end of the file, and stores that portion of the file in the fileBuffer variable. Finally, DeleteRecord writes the contents of fileBuffer starting at the beginning of the record to
be deleted.

Listing 2-3 demonstrates how you can use similar techniques to insert a record into a text-based database file.

Listing 2-3 Inserting a record in a database file

--choose file to work with
set pathToUse to choose file

try
   (* first put the record to be added into a variable; in this case 
   the record to be added is actually an AppleScript list because 
   the file on disk doesn't include label data *)
   set newRecord to ®
      {"Granny", "Smith", "123  Potato Chip Lane", ¬
      "Palo Minnow", "CA", "98761", "Snackable Computer", ¬
      "888-987-0987", "978 -234-5432", "123-985-1122"}
   set x to open for access pathToUse with write permission
   AddRecord(newRecord, 5, tab, return, x)
   close access x

on error errString number errNum
   display dialog errString
   close access x
end try
on AddRecord(recordToAdd, addWhere, fieldDelimiter, ¬ 
            recordDelimiter, fileRefNum)
   try
      --initialize variables
      set idx to 1 --counter
      set preRecordSize to 1 --offset of byte at which to add file
      set accumulatedSize to 0 --total size of records read 
      set numberOfFields to count of recordToAdd 
      if recordDelimiter is "" then
         set readxTimes to numberOfFields
      else
         set readxTimes to numberOfFields - 1
      end if

      (* if the record is to be added at the beginning of the file, 
      this If statement adds the record *)
      if addWhere is 1 then
         --read from beginning of file and store in postBuffer
         set postBuffer to read fileRefNum from 1
         (* before writing new record, file mark must be reset to 
         beginning of file; to do this, write an empty string to the 
         beginning of file *) 
         write "" to fileRefNum starting at 0
         WriteNewRecord(recordToAdd, fieldDelimiter, ¬
                        recordDelimiter, fileRefNum)
         --now add back the rest of the record
         write postBuffer to fileRefNum
         return
      end if
      (* if the record is to be added somewhere other than at the 
      beginning of the file, the rest of the AddRecord handler is 
      executed *)

      repeat with idx from 1 to addWhere - 1
         repeat (readxTimes) times
            set q to read fileRefNum until fieldDelimiter
            set accumulatedSize to accumulatedSize + (length of q)
         end repeat
         if readxTimes is not numberOfFields then
            set q to read fileRefNum until recordDelimiter
            set accumulatedSize to accumulatedSize + (length of q)
         end if
      end repeat
      (* read from beginning of file to the byte at which the new 
      record is to be added *)
      set postBuffer to read fileRefNum from accumulatedSize + 1
      
      (* before writing new record, set file mark to byte at which 
      new record is to be added; to do this, write an empty 
      string to that byte *)
      write "" to fileRefNum starting at accumulatedSize + 1
      WriteNewRecord(recordToAdd, fieldDelimiter, recordDelimiter, ¬
                     fileRefNum)
      --now add back the rest of the record
      write postBuffer to fileRefNum

   on error errString number errNum
      display dialog errString
   end try
end AddRecord

on WriteNewRecord(recordToAdd, fieldDelimiter, recordDelimiter,¬
                   fileRefNum)
   try
      set numberOfFields to count of recordToAdd
      if recordDelimiter is "" then
         set readxTimes to numberOfFields
      else
         set readxTimes to numberOfFields - 1
      end if
      
      repeat with idx from 1 to numberOfFields
         if idx Ê readxTimes then
            write item idx of recordToAdd & fieldDelimiter to ¬
               fileRefNum



         else 
            (* if file uses a record delimiter, write delimiter 
            after the last field in the record *)
            write item idx of recordToAdd & recordDelimiter to ¬
               fileRefNum
         end if
      end repeat
   on error errString number errNum
      display dialog errString
   end try
end WriteNewRecord
The AddRecord handler shown in Listing 2-3 takes five parameters:

recordToAdd
A list of the fields for the record to be added.
whichRecord
An integer that identifies the offset of the record you want
to add.
fieldDelimiter
The delimiter used in the file to separate fields.
recordDelimiter
The delimiter (if any) used to separate records. If the file doesn't use a different delimiter to separate records, this parameter must be set to "".
fileRefNum
A file reference number obtained with the Open for Access command.
If the new record is to be added at the beginning of the file, AddRecord reads all the records in the file and stores them in the postBuffer variable, then resets the file mark to the beginning of the file by writing an empty string to that location. This is a useful technique whenever you want to set the file mark without reading or writing any data.

Next, AddRecord uses the WriteNewRecord handler to write the record at the beginning of the file and writes the contents of the postBuffer variable after the new record. Note that the Write Command sets the end of file, so this example doesn't need to use the Get EOF and Set EOF commands.

If the new record is to be added somewhere other than at the beginning of the file, AddRecord uses a repeat loop to read through all the records that precede the new record's location. If recordDelimiter is set to "", AddRecord reads the specified number of fields for each record. If recordDelimiter is set to a delimiter, AddRecord reads all the fields in a record but the last, then reads the last field up to the record delimiter. The size of each successive record is added to the accumulatedSize variable, which contains the total size of the previously read records.

After it has stored, in the accumulatedSize variable, the total size of the records preceding the point at which the new record is to be added, AddRecord reads the remainder of the file and stores it in the postBuffer variable. It then resets the file mark to the byte at which the new record is to be added by writing an empty string to that location. After using the WriteNewRecord handler to write the record, AddRecord writes the contents of the postBuffer variable after the new record.

The WriteNewRecord handler shown in Listing 2-3 takes four parameters:

recordToAdd
A list of the fields for the record to be added.
fieldDelimiter
The delimiter used in the file to separate fields.
recordDelimiter
The delimiter (if any) used to separate records. If the file doesn't use a different delimiter to separate records, this parameter must be set to "".
fileRefNum
A file reference number obtained with the Open for Access command.
If recordDelimiter is set to "", WriteNewRecord includes a field delimiter after each field it writes. If recordDelimiter is set to a delimiter, WriteNewRecord includes a field delimiter after each field in a record but
the last and includes a record delimiter after the last field.

Listing 2-4 demonstrates one way to take advantage of the fact that the Open for Access command can create a file with a specified name in a specified location if the file doesn't already exist at that location.

Listing 2-4 Opening a file for write access and creating one if the file doesn't exist

on OpenFileIfItExists(theFile, writePermission)
   try
      (* if theFile doesn't exist, Info For returns error -43 *)
      set x to info for file theFile
      if writePermission is true then
         return (open for access file theFile with write permission)
      else
         return (open for access file theFile)
      end if
   on error theErrMsg number errorNum
      try
         --if error is -43, the user can choose to create the file
         display dialog "The file: " & theFile & " does not exist" ¬
            buttons {"Create It For Me", "Cancel", "Ok"} ¬
            default button 2
         if button returned of the result is "Ok" then
            return errorNum

         else
            --create the file
            if writePermission is true then
               return open for access file theFile ¬
                  with write permission
            else
               return open for access file theFile
            end if
         end if
      on error theErrMsg number theErrNumber
         return theErrNumber
      end try
   end try
end OpenFileIfItExists


--set a variable to the file you want to open or create
set fileToOpenOrCreate to "Hard Disk:Test File One"
set z to OpenFileIfItExists(fileToOpenOrCreate, true)
if z < 0 then
   --OpenFileIfItExists returned an error
   display dialog the result

else
   --OpenFileIfItExists returned a file reference number
   --do your work with the open file here
   close access z
end if
The OpenFileIfItExists handler shown in Listing 2-4 takes two parameters:

theFile
A string that consists of the full pathname for the file to open
or create.
writePermission
A Boolean value that indicates whether to open the file with (true) or without (false) write permission.
To determine whether the file exists or not, OpenFileIfItExists uses the Info For command. If the file doesn't exist, the Info For command returns
error -43, "File wasn't found," and OpenFileIfItExists displays a dialog box that allows the user to choose whether to create the new file. If the file exists or if it is successfully created, OpenFileIfItExists opens it with or without write permission, depending on the value of the writePermission parameter.


Previous Book Contents Book Index Next

© Apple Computer, Inc.
18 DEC 1996